01. iOS Crash的捕获

Crash分类

MacOS是一个类Unix的操作系统,内核由XNU构成,而XNU是基于NeXTSTEP和FreeBSD混合开发而成,系统架构图如下:

iOS系统架构图.png

不同的层次会产生不同的Crash, 根据Crash的不同来源,可以分为以下四类:

Mach异常

内核层的异常。用户态开发者可以通过 Mach API 设置 threadtaskhot 的异常端口来捕获 Mach 异常。

这些内核对象,对于Mach来说都是一个个的Object,这些Objects基于Mach实现自己的功能,并通过Mach Message来进行通信,Mach提供了相关的应用层的API来操作。

与 Mach 异常相关的 API 有:

如何捕获Mach异常

Mach异常捕获.png

要捕获Mach异常,需要新建一个监控线程,在监控线程中监听 Mach 异常并处理异常信息。

static mach_port_t server_port;
static void *exc_handler(void *ignored);

//判断是否 Xcode 联调
bool ksdebug_isBeingTraced(void)
{
    struct kinfo_proc procInfo;
    size_t structSize = sizeof(procInfo);
    int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
    
    if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
    {
        return false;
    }
    
    return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}

#define EXC_UNIX_BAD_SYSCALL 0x10000 /* SIGSYS */
#define EXC_UNIX_BAD_PIPE    0x10001 /* SIGPIPE */
#define EXC_UNIX_ABORT       0x10002 /* SIGABRT */
static int signalForMachException(exception_type_t exception, mach_exception_code_t code)
{
    switch(exception)
    {
        case EXC_ARITHMETIC:
            return SIGFPE;
        case EXC_BAD_ACCESS:
            return code == KERN_INVALID_ADDRESS ? SIGSEGV : SIGBUS;
        case EXC_BAD_INSTRUCTION:
            return SIGILL;
        case EXC_BREAKPOINT:
            return SIGTRAP;
        case EXC_EMULATION:
            return SIGEMT;
        case EXC_SOFTWARE:
        {
            switch (code)
            {
                case EXC_UNIX_BAD_SYSCALL:
                    return SIGSYS;
                case EXC_UNIX_BAD_PIPE:
                    return SIGPIPE;
                case EXC_UNIX_ABORT:
                    return SIGABRT;
                case EXC_SOFT_SIGNAL:
                    return SIGKILL;
            }
            break;
        }
    }
    return 0;
}

static NSString *stringForMachException(exception_type_t exception) {
    switch(exception)
    {
        case EXC_ARITHMETIC:
            return @"EXC_ARITHMETIC";
        case EXC_BAD_ACCESS:
            return @"EXC_BAD_ACCESS";
        case EXC_BAD_INSTRUCTION:
            return @"EXC_BAD_INSTRUCTION";
        case EXC_BREAKPOINT:
            return @"EXC_BREAKPOINT";
        case EXC_EMULATION:
            return @"EXC_EMULATION";
        case EXC_SOFTWARE:
        {
            return @"EXC_SOFTWARE";
            break;
        }
    }
    return 0;
}

void installExceptionHandler() {
    if (ksdebug_isBeingTraced()) {
        // 当前正在调试状态, 不启动 mach 监听
        return ;
    }
    kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
    assert(kr == KERN_SUCCESS);
    
    kern_return_t rc = 0;
    exception_mask_t excMask = EXC_MASK_BAD_ACCESS |
    EXC_MASK_BAD_INSTRUCTION |
    EXC_MASK_ARITHMETIC |
    EXC_MASK_SOFTWARE |
    EXC_MASK_BREAKPOINT;
    
    rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "------->Fail to allocate exception port\\\\\\\\n");
        return;
    }
    
    rc = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "-------->Fail to insert right");
        return;
    }
    
    rc = thread_set_exception_ports(mach_thread_self(), excMask, server_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
    if (rc != KERN_SUCCESS) {
        fprintf(stderr, "-------->Fail to  set exception\\\\\\\\n");
        return;
    }
    
    //建立监听线程
    pthread_t thread;
    pthread_create(&thread, NULL, exc_handler, NULL);
}

static void *exc_handler(void *ignored) {
    // Exception handler – runs a message loop. Refactored into a standalone function
    // so as to allow easy insertion into a thread (can be in same program or different)
    mach_msg_return_t rc;
    fprintf(stderr, "Exc handler listening\\\\\\\\n");
    // The exception message, straight from mach/exc.defs (following MIG processing) // copied here for ease of reference.
    typedef struct {
        mach_msg_header_t Head;
        /* start of the kernel processed data */
        mach_msg_body_t msgh_body;
        mach_msg_port_descriptor_t thread;
        mach_msg_port_descriptor_t task;
        /* end of the kernel processed data */
        NDR_record_t NDR;
        exception_type_t exception;
        mach_msg_type_number_t codeCnt;
        integer_t code[2];
        int flavor;
        mach_msg_type_number_t old_stateCnt;
        natural_t old_state[144];
    } Request;
    
    Request exc;

    struct rep_msg {
        mach_msg_header_t Head;
        NDR_record_t NDR;
        kern_return_t RetCode;
    } rep_msg;
    
    for(;;) {
        // Message Loop: Block indefinitely until we get a message, which has to be
        // 这里会阻塞,直到接收到exception message,或者线程被中断。
        // an exception message (nothing else arrives on an exception port)
        rc = mach_msg( &exc.Head,
                      MACH_RCV_MSG|MACH_RCV_LARGE,
                      0,
                      sizeof(Request),
                      server_port, // Remember this was global – that's why.
                      MACH_MSG_TIMEOUT_NONE,
                      MACH_PORT_NULL);
        
        if(rc != MACH_MSG_SUCCESS) {
            /*... */
            break ;
        };
        
        //Mach Exception 类型
        NSMutableString *crashInfo = [NSMutableString stringWithFormat:@"mach exception:%@ %@\n\n",stringForMachException(exc.exception), stringForSignal(signalForMachException(exc.exception, exc.code[0]))];
        
        rep_msg.Head = exc.Head;
        rep_msg.NDR = exc.NDR;
        rep_msg.RetCode = KERN_FAILURE;
        
        kern_return_t result;
        if (rc == MACH_MSG_SUCCESS) {
            result = mach_msg(&rep_msg.Head,
                              MACH_SEND_MSG,
                              sizeof (rep_msg),
                              0,
                              MACH_PORT_NULL,
                              MACH_MSG_TIMEOUT_NONE,
                              MACH_PORT_NULL);
        }
        //移除其他 Crash 监听, 防止死锁
        NSSetUncaughtExceptionHandler(NULL);
        signal(SIGHUP, SIG_DFL);
        signal(SIGINT, SIG_DFL);
        signal(SIGQUIT, SIG_DFL);
        signal(SIGABRT, SIG_DFL);
        signal(SIGILL, SIG_DFL);
        signal(SIGSEGV, SIG_DFL);
        signal(SIGFPE, SIG_DFL);
        signal(SIGBUS, SIG_DFL);
        signal(SIGPIPE, SIG_DFL);
    }
    
    return  NULL;
}

注意:避免在 Xcode 联调时监听,会死锁。原因是我们监听了EXC_BREAKPOINT这类型的Exception,一旦启动 app 联调后, 会立即触发EXC_BREAKPOINT。而这段代码处理完后,会进入下一个循环等待,可主线程这是还等着消息处理结果,这就造成等待死锁。


Unix 信号

Mach已经通过异常机制提供了底层的异常处理,但为了兼容更为流行的POSIX标准,BSD在Mach异常机制之上构建的UNIX信号处理机制。

UNIX信号又称 BSD 信号,如果开发者没有捕获 Mach 异常,则会被 host 层的方法 ux_exception() 将异常转换为对应的 Unix 信号,并通过方法 threadsignal() 将信号投递到出错线程。可以同 signal(x, SignalHandler) 来捕获 signal

Unix信号异常捕获.png

UNIX信号列表

UNIX信号实际上是由Mach port抛出的信号转化的,一共有31种信号

Tips: 在终端输入 kill -l 查看所有的 signal 信号。

信号 含义
SIGHUP 挂起
SIGINT 程序终止信号 interrupt,在用户键入 INTR 字符(通常是 Ctrl-C)是发出,用于通知前台进程组终止进程
SIGQUIT 程序退出信号 quit,由 QUIT 字符来控制(通常是Ctrl-),程序在收到该信号退出时会生成 core 文件
SIGILL 执行非法指令,当一个进程尝试执行一个非法指令时发送给它的信号
SIGTRAP 由断点指令或陷阱指令
SIGABRT 程序打断信号 abort
SIGFPE 致命的算术运算错误
SIGKILL 立即结束程序的运行。不能被阻塞、处理和忽略
SIGBUS 非法地址
SIGSEGV 无效内存访问
SIGSYS 非法的系统调用
SIGPIPE 管道破裂。进程间的通信,如管道的异常读写
SIGALRM alarm 发出的信号
SIGTERM 终止信号,可被阻塞和处理。通常用来要求程序自己正常退出
SIGURG I/O有紧急数据达到当前进程
SIGSTOP 进程停止
SIGTSTP 进程停止
SIGCONT 进程继续
SIGCHLD 子进程退出
SIGTTIN 进程停止,后台进程从终端读数据时
SIGTTOU 进程停止,后台进程想终端写数据时
SIGIO I/O相关
SIGXCPU 进程的CPU时间篇到期
SIGXFSZ 文件大小超出上限
SIGVTALRM 虚拟时钟超时
SIGPROF profile 时钟超时
SIGWINCH 窗口大小改变
SIGUSR1 用户信号1
SIGUSR2 用户信号2
SIGSTKFLT 协处理器栈故障,栈溢出,在内存耗尽时,一般 malloc 返回 NULL 且设置 errno 为 ENOMEM,但有些系统可能会使用 SIGSTKFLT 信号代替
SIGPWR 关机

如何捕获Unix信号

捕获信号:


// 一般需要捕获的信号
static const int g_fatalSignals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};

void installSignalHandler() {
    stack_t ss;
    struct sigaction sa;
    struct timespec req, rem;
    long ret;
    // 申请一块内存空间作为可选的信号处理函数栈使用
    ss.ss_flags = 0;
    ss.ss_size = SIGSTKSZ;
    ss.ss_sp = malloc(ss.ss_size);
    // 使用 sigaltstack 函数通知系统可选的信号处理栈帧的存在及其位置
    sigaltstack(&ss, NULL);
    // 指定 SA_ONSTACK 标志通知系统这个信号处理函数应该在可选的栈帧上面执行注册的信号处理函数
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handleSignalException;
    sa.sa_flags = SA_ONSTACK;
    sigaction(SIGABRT, &sa, NULL);
}

void XXXHandleSignalException(int signal) {
    // 打印堆栈
    NSMutableString *crashInfo = [[NSMutableString alloc] init];
    [crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
    [crashInfo appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [crashInfo appendFormat:@"%s\n", strs[I]];
    }
    NSLog(@"%@", crashInfo);
    // 移除其他 Crash 监听, 防止死锁
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

NSException

应用层的异常,未被捕获的异常,导致程序向自身发送了 SIGABRT 信号而崩溃,是应用程序自己可控的。对于未被捕获的异常,是可以通过 try-catch 或 NSSetUncaughtExceptionHandler() 机制类捕获的。

常见的 Exception:

捕获 NSExpection:

// 记录之前的Crash回调函数(如果有的话)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

+ (void)registerUncaughtExceptionHandler {
    // 将别人之前注册的Crash回调取出并备份
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    // 然后再注册自己的
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

// 崩溃时的回调函数
static void UncaughtExceptionHandler(NSException * exception) {
    // 异常的堆栈信息
    NSArray *stackInfo = [exception callStackSymbols];
    // 出现异常的原因
    NSString *reason = [exception reason];
    // 异常名称
    NSString *name = [exception name];
    // 异常错误报告
    NSString *exceptionInfo = [NSString stringWithFormat:@"uncaughtException异常错误报告:\n name:%@\n reason:\n %@\n callStackSymbols:\n %@", name, reason, [stackInfo componentsJoinedByString:@"\n"]];
    // 保存Crash日志到沙盒cache目录
    [SKTool cacheCrashLog:exceptionInfo name:@"CrashLog(UncaughtException)"];
    // 在自己handler处理完后记得把别人的handler注册回去,形成规范的SOP
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
    // 杀掉程序,这样可以防止同时抛出的SIGABRT被Signal异常捕获
    kill(getpid(), SIGKILL);
}

C++异常

系统捕获到 C++ 异常后会将其转换为 OC 异常抛出,此时的调用堆栈是在异常发生时的队长;但若转换失败则会调用 __cxa_throw 抛出异常,此时的调用队长是处理异常的堆栈,导致原始异常调用堆栈丢失。 捕获 C++ 异常:

  1. 设置异常处理函数:
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);

调用 set_terminate(CPPExceptionTerminate) 设置新的全局终止处理函数并保持旧的函数。

  1. 重写 __cxa_throw
void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) { // 获取调用堆栈并存储 // 再调用原始的 __cxa_throw 函数 }
  1. 异常处理函数
    __cxa_throw 往后执行,进入 set_terminate 设置的异常梳理函数。判断如果是 OC 异常则什么也不多,让 OC 异常机制处理;否则获取异常信息

引用文献

史上最全 iOS Crash/崩溃/异常 捕获